Skip to content

refactor(cli): clean up compilation config and registry cursor#1367

Merged
sergio-sisternes-epam merged 1 commit into
microsoft:mainfrom
altf4-games:refactor/minor-cleanups
May 26, 2026
Merged

refactor(cli): clean up compilation config and registry cursor#1367
sergio-sisternes-epam merged 1 commit into
microsoft:mainfrom
altf4-games:refactor/minor-cleanups

Conversation

@altf4-games
Copy link
Copy Markdown
Contributor

refactor(cli): modernize config instantiation and enforce registry spec compliance

TL;DR

This PR replaces the manual CompilationConfig mapping in the CLI with dataclasses.replace() to prevent field-drift bugs. It also drops the legacy snake_case next_cursor fallback in the registry client to enforce strict adherence to the v0.1 MCP registry spec.

Problem (WHY)

  • Fragility in configuration overrides: The compilation CLI manually mapped CompilationConfig fields during dry runs. Adding a new field to configuration ran the risk of being silently dropped during dry-run processing.
  • Spec drift: The registry client accepted a legacy next_cursor key from search metadata to support outdated in-tree mock fixtures, drifting from the exact v0.1 spec payload.
  • As stated in Agent Skills, "agents pattern-match well against concrete structures". Removing legacy abstractions aligns the codebase structurally with the actual specifications, preventing future contextual drift.

Approach (WHAT)

  • Replaced the manual instantiation definition with the native dataclasses.replace module for CompilationConfig.
  • Refactored SimpleRegistryClient to extract only the explicitly supported nextCursor key from pagination.

Implementation (HOW)

File Changes
src/apm_cli/commands/compile/cli.py Added import dataclasses and swapped the manual CompilationConfig(...) instantiation in the dry-run strategy to dataclasses.replace(config, dry_run=True, strategy="single-file").
src/apm_cli/registry/client.py Dropped the fallback or metadata.get("next_cursor") assignment, natively resolving the spec-compliant "nextCursor" key from the API metadata.

Diagrams

Note

Legend: The CLI compilation dry-run now strictly clones the existing configuration context using standard dataclass replacement mechanisms.

flowchart LR
    A[Compile Command] -->|"dry_run=True"| B(dataclasses.replace)
    B -->|'strategy=single-file'| C[intermediate_config]
    C --> D[AgentsCompiler.compile]
Loading

Trade-offs

  • Any external integration or mock test relying on the legacy next_cursor JSON metadata fallback shape will now fail and must be updated to the v0.1 spec (nextCursor).

Benefits

  1. Maintainability: New attributes added to CompilationConfig will automatically propagate through dry-runs without requiring synchronous CLI mapping updates.
  2. Standardization: Removing the legacy fallback corrects our payload parsers natively to the v0.1 registry standards.

Validation

Validation Output
> uv run --extra dev ruff check src/ tests/
All checks passed!

> uv run --extra dev ruff format --check src/ tests/
102 files already formatted

> uv run pytest tests/unit/test_registry_client.py
49 passed in 8.46s

Scenario Evidence:

  • Scenario: Single-file compilation fallback mutation natively propagates config paths.
  • Test: Configuration structure is tested dynamically during apm compile evaluation.
  • Principle: Grounding outputs in deterministic execution transforms probabilistic generation into verifiable action.

How to test

  • Check out the branch locally.
  • Run format and linter checks to ensure parity: uv run --extra dev ruff check src/ tests/ (Passed as shown in Validation output)
  • Run the registry tests suite to ensure pagination queries successfully execute against mock constraints: uv run pytest tests/unit/test_registry_client.py (Passed as shown in Validation output)
  • Optionally run apm compile on a dummy package file to assert that dry run instantiation invokes without exceptions.

Copilot AI review requested due to automatic review settings May 18, 2026 12:15
@altf4-games
Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR refactors the CLI compilation dry-run config cloning to use dataclasses.replace() (avoiding field drift as CompilationConfig evolves) and tightens registry pagination parsing to only accept the spec-compliant metadata.nextCursor.

Changes:

  • Swap manual CompilationConfig(...) field mapping for dataclasses.replace(config, ...) in single-file dry-run compilation.
  • Enforce v0.1 MCP registry spec compliance by removing legacy next_cursor pagination fallback.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/apm_cli/commands/compile/cli.py Uses dataclasses.replace() to derive the intermediate dry-run config without manually mapping fields.
src/apm_cli/registry/client.py Removes acceptance of legacy snake_case cursor key, reading only metadata.nextCursor.

@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label May 24, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel: ship_now

Cleanup PR closes a silent config-field propagation bug (dataclasses.replace) and removes a resolved legacy cursor fallback; both changes reduce latent defect surface with no user-visible breakage.

All six active mandatory panelists converged without conflict: no blocking, no recommended-tier concerns from python-architect, cli-logging-expert, devx-ux-expert, or supply-chain-security-expert. The dataclasses.replace() adoption is a correctness fix masquerading as a cleanup -- the old manual copy silently defaulted eight fields (local_only, trace, debug, exclude, single_agents, min_instructions_per_file, source_attribution, clean_orphaned) whenever the single-file intermediate config was constructed, meaning users who passed --local-only or --trace got silently ignored flags mid-compile. That bug is now structurally impossible. The next_cursor snake_case removal closes a TODO(v0.1) that was deferred at the v0.1 boundary; the camelCase path is regression-trapped (test_list_servers_reads_nextCursor_camelCase, assertion: self.assertEqual(next_cursor, 'spec-cursor')) and the removed fallback had no test coverage of its own, confirming it was dead code.

The oss-growth-hacker raised one recommended finding: the cursor fallback removal warrants a one-line CHANGELOG entry under Removed for any external registry implementors who might have relied on the snake_case key. This is the only finding above nit tier and it is advisory, not blocking. The python-architect flagged a pre-existing __post_init__ fragility (dataclasses.replace() retriggers it) as a nit; the current PR does not introduce or worsen that fragility. The test-coverage-expert confirmed the existing CLI regression trap passes (test_codex_single_agents_no_claude_preview_issue_765_cli, outcome: passed) and noted only a nit gap: no test asserts non-default field preservation through replace(). Both test evidence rows carry outcome: passed on the surviving paths; no failed or missing evidence on a secure-by-default or governed-by-policy surface.

The growth signal is worth noting: the silent config-field drop fix is a usable reliability proof point ("compile flags now propagate reliably through all single-file paths") for future release notes, and the cleanup PR itself models the kind of small, safe, well-scoped contribution APM wants to attract. A CHANGELOG chore entry and an optional CONTRIBUTING.md callout for the dataclasses.replace() idiom are the only follow-ups worth scheduling.

Aligned with: Pragmatic as npm -- dataclasses.replace() is stdlib-idiomatic Python; the fix removes a footgun (silent field defaulting) that a pragmatic tool must not have on a primary compile path. OSS community driven -- closing a TODO(v0.1) on schedule and logging it in CHANGELOG models the hygiene that external contributors learn from and emulate.

Growth signal. The silent config-field propagation fix ("compile flags now propagate reliably through all single-file paths") is a concrete reliability proof point for future release notes and contributor storytelling. Bundle into the next patch release note rather than a standalone announcement.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 1 Both changes are correct and well-scoped. dataclasses.replace() closes a silent field-default hole; next_cursor cleanup removes dead code. No architectural concerns.
CLI Logging Expert 0 0 0 Neither change touches user-facing CLI output, logging, STATUS_SYMBOLS, CommandLogger, or any console helper. No findings.
DevX UX Expert 0 0 0 Neither change touches CLI surface, command flags, error wording, help text, or install/run flows. Correctness improvement only.
Supply Chain Security 0 0 0 No supply chain concerns. stdlib dataclasses adds no dependency; cursor key alignment has no trust-boundary implications.
OSS Growth Hacker 0 1 1 Pure internal cleanup. CHANGELOG entry for cursor removal recommended. dataclasses.replace() idiom worth surfacing for contributors.
Test Coverage Expert 0 0 2 Existing CLI regression trap covers the compile path. Camelcase cursor path regression-trapped. Nit gap: no test asserts non-default field preservation through replace().

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 3 follow-ups

  1. [OSS Growth Hacker] Add a one-line CHANGELOG entry under Removed for the next_cursor snake_case fallback drop -- Any external registry implementor relying on the snake_case key gets a visible upgrade path; models OSS hygiene for new contributors. Only recommended-tier finding in the entire panel.
  2. [Test Coverage Expert] Add a unit test asserting non-default field preservation through dataclasses.replace() -- The existing regression trap (test_codex_single_agents_no_claude_preview_issue_765_cli) does not set local_only, trace, debug, or exclude to non-default values; a future regression where one of those fields begins defaulting again would be invisible to CI. Low-cost test, high-value guardrail.
  3. [Python Architect] Add a comment in CompilationConfig.__post_init__ noting that dataclasses.replace() retriggers it -- Pre-existing fragility: if a future caller does replace(config, single_agents=False, strategy='distributed'), __post_init__ will not override strategy because single_agents is False. A comment makes the precedence contract explicit and prevents misuse.

Architecture

classDiagram
    direction LR
    class CompilationConfig {
        <<Dataclass>>
        +output_path str
        +chatmode str
        +resolve_links bool
        +dry_run bool
        +with_constitution bool
        +target CompileTargetType
        +strategy str
        +single_agents bool
        +trace bool
        +local_only bool
        +debug bool
        +min_instructions_per_file int
        +source_attribution bool
        +clean_orphaned bool
        +exclude list
        +__post_init__()
        +from_apm_yml(**overrides) CompilationConfig
    }
    class AgentsCompiler {
        <<Service>>
        +compile(config) CompilationResult
    }
    class CompilationResult {
        <<ValueObject>>
        +success bool
        +content str
    }
    class compile_cli {
        <<IOBoundary>>
        +run(config)
    }
    class ConstitutionInjector {
        <<Service>>
        +inject(content, with_constitution, output_path)
    }
    compile_cli ..> CompilationConfig : creates via dataclasses.replace
    compile_cli *-- AgentsCompiler : calls
    compile_cli *-- ConstitutionInjector : calls
    AgentsCompiler ..> CompilationConfig : reads
    AgentsCompiler ..> CompilationResult : returns
    class CompilationConfig:::touched
    class compile_cli:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm compile - single-file branch]) --> B[load CompilationConfig from apm.yml + CLI flags]
    B --> C{strategy == single-file?}
    C -- No --> D[distributed compilation path - not in scope]
    C -- Yes --> E["dataclasses.replace(config, dry_run=True, strategy=single-file)\n=> intermediate_config (all 14 fields propagated)"]
    E --> F["AgentsCompiler.compile(intermediate_config)\n=> intermediate_result"]
    F --> G{intermediate_result.success?}
    G -- No --> H[surface error to user]
    G -- Yes --> I[ConstitutionInjector.inject]
    I --> J{dry_run flag?}
    J -- Yes --> K[print content, skip write]
    J -- No --> L[write final_content to output_path]
Loading

Recommendation

Panel is unanimous at the nit level or below. The correctness fix (dataclasses.replace) and the dead-code removal (next_cursor fallback) are both safe, well-scoped, and regression-trapped. The three follow-ups are all nit or recommended tier and none require pre-merge resolution. Ship now; file the CHANGELOG entry and the field-preservation test as a fast-follow in the same sprint.


Full per-persona findings

Python Architect

  • [nit] CompilationConfig.__post_init__ side-effect interacts with dataclasses.replace() at src/apm_cli/compilation/agents_compiler.py:91
    __post_init__ runs after every dataclasses.replace() call. The replace sets strategy='single-file' explicitly, and single_agents is copied from config. If the original config had single_agents=True, __post_init__ will overwrite strategy to 'single-file' again -- harmless here. But if a future caller does dataclasses.replace(config, single_agents=False, strategy='distributed'), __post_init__ would NOT override strategy. The current PR is safe; the fragility is pre-existing.
    Suggested: # NOTE: dataclasses.replace() also triggers __post_init__; ensure flag precedence logic remains idempotent.

CLI Logging Expert

No findings.

DevX UX Expert

No findings.

Supply Chain Security

No findings.

OSS Growth Hacker

  • [recommended] No CHANGELOG entry for the next_cursor snake_case fallback removal at src/apm_cli/registry/client.py
    If any external registry implementors exist, dropping the snake_case 'next_cursor' fallback silently breaks them with no upgrade path visible in the changelog. A one-line entry under Removed or Changed models good OSS hygiene and signals to contributors that even small removals are documented.
  • [nit] dataclasses.replace() idiom could be highlighted in contributor docs at src/apm_cli/commands/compile/cli.py
    The fix is a good contributor pattern -- stdlib over manual field copying. Worth a comment in the code or a CONTRIBUTING.md callout so new contributors learn this idiom and apply it elsewhere. No action required in this PR.

Auth Expert -- inactive

No auth surface touched: changed files are compile/cli.py (internal config refactor) and registry/client.py (cursor key cleanup), neither affecting token management, credential resolution, or AuthResolver.

Doc Writer -- inactive

No documentation surface touched; internal cleanup only (dataclasses.replace refactor and legacy cursor fallback removal).

Test Coverage Expert

  • [nit] CLI test for dataclasses.replace() does not assert non-default field preservation at tests/unit/compilation/test_compile_target_flag.py
    The existing test (test_codex_single_agents_no_claude_preview_issue_765_cli) invokes apm compile --target codex --single-agents --dry-run and asserts exit code 0, AGENTS.md in output, and absence of Claude preview text. However, the test never sets fields like local_only, trace, debug, exclude, min_instructions_per_file, source_attribution, or clean_orphaned to non-default values. The dataclasses.replace() change is correct, but no test would catch a future regression where one of those fields begins defaulting again.
    Suggested: Add a unit test that constructs CompilationConfig with e.g. local_only=True, trace=True, then calls dataclasses.replace(config, dry_run=True, strategy='single-file') and asserts all non-overridden fields are unchanged.
    Proof (passed): tests/unit/compilation/test_compile_target_flag.py::test_codex_single_agents_no_claude_preview_issue_765_cli -- proves: CLI compile --target codex --single-agents --dry-run routes to AGENTS.md without appending Claude preview text [devx]
    assert 'AGENTS.md' in result.output and 'CLAUDE.md Preview' not in result.output
  • [nit] No test for removed snake_case next_cursor fallback -- deletion is clean at tests/unit/test_registry_client.py
    Probe of tests/unit/test_registry_client.py for snake_case next_cursor found no test for the removed fallback path. The camelCase path is regression-trapped at line 65. Dead-code removal has no test gap to close. No action required.
    Proof (passed): tests/unit/test_registry_client.py::test_list_servers_reads_nextCursor_camelCase -- proves: metadata.nextCursor (camelCase, the spec key) is read and returned correctly by list_servers() [vendor-neutral]
    self.assertEqual(next_cursor, 'spec-cursor')

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.

Note

🔒 Integrity filter blocked 3 items

The following items were blocked because they don't meet the GitHub integrity level.

  • #1367 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • refactor(cli): clean up compilation config and registry cursor #1367 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • #1367 search_pull_requests: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1367 · ● 2.1M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label May 24, 2026
@sergio-sisternes-epam sergio-sisternes-epam added this pull request to the merge queue May 26, 2026
Merged via the queue into microsoft:main with commit 6ff30b2 May 26, 2026
12 checks passed
@danielmeppiel danielmeppiel mentioned this pull request May 26, 2026
danielmeppiel added a commit that referenced this pull request May 26, 2026
* chore: cut 0.15.0

Move Unreleased -> [0.15.0] - 2026-05-27 and bump pyproject + uv.lock.

Audit applied: every PR merged since v0.14.2 has exactly one
changelog entry; each entry leads with the user-visible impact.

Fixes during audit:
- Add missing entries for #1367, #1403, #1465, #1487, #1492, #1462,
  #1477, #1439, #1484, and the 131679f follow-up commit.
- Collapse the two #1473 lines into one.
- Merge the #1476 Security/GitCache-hardening entry into its Added
  entry (same PR, one logical change).
- Replace bogus #1243 PR ref with the actual merge PR #1308 for the
  persisted transport-flag config.
- Relocate the #1324-delivered marketplace CLI entries (apm pack
  --marketplace / --marketplace-path / --json, outputs map form)
  out of Unreleased and into [0.14.2], where they actually shipped.
  They were mis-attributed to #1317 and orphaned across the 0.14.2
  cut.

Verified locally: ruff check + ruff format --check both clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants